/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.tools.idea.run.deployment.liveedit.analysis

import com.android.annotations.Trace
import com.android.tools.idea.run.deployment.liveedit.analysis.leir.IrInstruction
import com.android.tools.idea.run.deployment.liveedit.analysis.leir.IrInstructionList
import org.jetbrains.org.objectweb.asm.Opcodes

private const val kComposerClass = "androidx/compose/runtime/ComposerKt"

// sourceInformation() is a Compose debugging method that allows for integration with tooling, such as the inspector. It accepts a single
// String parameter that contains information about the source location of calls to @Composable functions; this causes the String to change
// frequently as modifications are made to the file, which is why we ignore it.
//
// See: https://developer.android.com/reference/kotlin/androidx/compose/runtime/package-summary#sourceInformation(androidx.compose.runtime.Composer,kotlin.String)
private const val kSourceInfo = "sourceInformation"

// sourceInformationMarkerStart() is a similar method to sourceInformation(), but accepts an additional integer key parameter generated by
// the compiler based on the source location of the call.
//
// See: https://developer.android.com/reference/kotlin/androidx/compose/runtime/package-summary#sourceInformation(androidx.compose.runtime.Composer,kotlin.String)
private const val kSourceInfoMarker = "sourceInformationMarkerStart"

// traceEventStart() is called in order to add composition method traces to a Perfetto trace. It accepts a string argument that contains
// source offset information, which causes similar issues to sourceInformation().
//
// Technically there are more arguments passed to traceEventStart(); thankfully, since the JVM is a stack machine, the string argument is
// always in the same position in the bytecode (in dex, the LDC operation may be moved elsewhere). If we wanted to be perfect, we'd actually
// ignore every instruction in the isTraceInProgress() block; however, that's more complicated and thus harder to get correct. Since the
// string argument is the only part of the block that changes with source offset, filtering that should be sufficient.
private const val kTraceEventStart = "traceEventStart"

/**
 * Checks if the only differences between the two instruction lists are constants passed to Compose debugging methods. Compose generates
 * multiple debugging instrumentation calls that take String parameters based on source file offset positions; this means that simple
 * changes to a large file may result in dozens of changed methods. We filter these changes for to avoid invalidating a large number of
 * group IDs on every edit.
 */
@Trace
internal fun onlyComposeDebugConstantChanges(old: IrInstructionList, new: IrInstructionList): Boolean {
  var oldInsn = old.first
  var newInsn = new.first
  while (oldInsn != null || newInsn != null) {
    if (oldInsn == newInsn) {
      oldInsn = oldInsn?.nextInsn
      newInsn = newInsn?.nextInsn
      continue
    }

    // At this point, both cannot be null. If either is null, we've added/removed an instruction, and can bail out of further checks.
    if (oldInsn == null || newInsn == null) {
      return false
    }

    if (oldInsn.opcode != Opcodes.LDC || newInsn.opcode != Opcodes.LDC) {
      return false
    }

    // Both constants must be followed by a non-null instruction; Compose debugging constants are passed to Composer API calls.
    var oldNextInsn = oldInsn.nextInsn ?: return false
    var newNextInsn = newInsn.nextInsn ?: return false

    if (oldInsn.params[0] is String && newInsn.params[0] is String) {
      // If both constants are strings, check to see if they're being passed to Compose debugging calls.
      val isSourceInfoCall = isSourceInfoCall(oldNextInsn) && isSourceInfoCall(newNextInsn)
      val isSourceInfoMarkerCall = isSourceInfoMarkerCall(oldNextInsn) && isSourceInfoMarkerCall(newNextInsn)
      val isTraceEventCall = isTraceEventCall(oldNextInsn) && isTraceEventCall(newNextInsn)

      if (!isSourceInfoCall && !isSourceInfoMarkerCall && !isTraceEventCall) {
        return false
      }
    }
    else if (oldInsn.params[0] is Int && newInsn.params[0] is Int) {
      // If both constants are ints, we're only checking for a call to sourceInformationMarkerStart(). The next instruction is expected to
      // be a constant string, so we can skip it; it's not particularly relevant if it is or not, as long as we see if the method invocation
      // is what we expect
      oldNextInsn = oldNextInsn.nextInsn ?: return false
      newNextInsn = newNextInsn.nextInsn ?: return false

      if (!isSourceInfoMarkerCall(oldNextInsn) || !isSourceInfoMarkerCall(newNextInsn)) {
        return false
      }
    }
    else {
      // If the constants are anything else, we can safely return; it's not a source information related call.
      return false
    }

    oldInsn = oldInsn.next
    newInsn = newInsn.next
  }

  return true
}

private fun isSourceInfoCall(insn: IrInstruction) = insn.opcode == Opcodes.INVOKESTATIC &&
                                                    insn.params[0] as? String == kComposerClass &&
                                                    insn.params[1] as? String == kSourceInfo

private fun isSourceInfoMarkerCall(insn: IrInstruction) = insn.opcode == Opcodes.INVOKESTATIC &&
                                                          insn.params[0] as? String == kComposerClass &&
                                                          insn.params[1] as? String == kSourceInfoMarker

private fun isTraceEventCall(insn: IrInstruction) = insn.opcode == Opcodes.INVOKESTATIC &&
                                                    insn.params[0] as? String == kComposerClass
                                                    && insn.params[1] as? String == kTraceEventStart

